use std::io;

use openmls_traits::key_store::MlsEntityId;
use sea_orm::prelude::Uuid;
use sled::transaction::{ConflictableTransactionError, TransactionError};
use thiserror::Error;

use aspect_core::message_queue::MessageQueueId;
use aspect_ident::{device, DeviceIdent};

#[derive(Debug, Error)]
pub enum Error {
    /// If this happens it means that you should check to see if that's an equivocation.
    #[error("conflicting DIR in database")]
    ConflictingDir,

    /// Tried to write a DIR that's older than the one that's currently in the
    /// database.  This might be an equivocation against an older DIR that we've
    /// since purged.
    #[error("tried to write DIR")]
    StaleDir,

    #[error("tried to overwrite equivocation report")]
    OverwriteEquivocation,

    #[error("unknown device identity {0}")]
    UnknownIdent(device::DeviceIdent),

    #[error("user '{0}' unknown")]
    UnknownUser(String),

    #[error("username '{0}' too long")]
    UsernameTooLong(String),

    #[error("user with name already exists '{0}'")]
    UserExists(String),

    #[error("tried to add existing device to user '{0}'")]
    DeviceExists(DeviceIdent),

    #[error("password incorrect/invalid")]
    Password,

    #[error("unknown job with ID {0}")]
    UnknownJob(Uuid),

    /// This should be recoverable when we restart.
    #[error("all workers for job queue '{0}' exited")]
    JobWorkersExited(String),

    #[error("device {0} already claimed by UID {1}")]
    DeviceAlreadyClaimed(DeviceIdent, i32),

    #[error("ident: {0}")]
    Ident(#[from] aspect_ident::errors::Error),

    #[error("orm(db): {0}")]
    OrmDb(#[from] sea_orm::DbErr),

    #[error("orm(sql): {0}")]
    OrmSql(#[from] sea_orm::SqlErr),

    #[error("orm(col): {0}")]
    OrmCol(#[from] sea_orm::ColumnFromStrErr),

    #[error("task '{0}' aborted")]
    BlockingTaskAborted(&'static str),

    #[error("task '{0}' paniced")]
    BlockingTaskPanic(&'static str),

    #[error("llmq: {0}")]
    Llmq(#[from] aspect_db_mq_types::Error),

    #[error("llmq db: {0}")]
    LlmqDb(#[from] aspect_db_llmq::DbError),

    #[error("unknown message queue '{0}'")]
    UnknownMessageQueue(MessageQueueId),

    #[error("device '{0}' missing sub to queue '{1}'")]
    DeviceMissingQueue(DeviceIdent, MessageQueueId),

    #[error("unknown error inserting '{0}' sub to queue '{1}'")]
    UnableToInsertSub(DeviceIdent, MessageQueueId),

    #[error("missing expected stateprop key '{0}'")]
    MissingKey(String),

    #[error("io: {0}")]
    Io(#[from] io::Error),

    #[error("codec: {0}")]
    Codec(#[from] aspect_codec::CodecError),

    #[error("not yet implemented")]
    Unimplemented,

    #[error("{0}")]
    Other(String),
}

impl From<password_hash::Error> for Error {
    // TODO maybe make this actually expose some details?
    fn from(_: password_hash::Error) -> Self {
        Self::Password
    }
}

#[derive(Debug, Error)]
pub enum DbError {
    #[error("ident: {0}")]
    Ident(#[from] Error),

    #[error("db: {0}")]
    Sled(#[from] sled::Error),
}

impl From<aspect_codec::CodecError> for DbError {
    fn from(value: aspect_codec::CodecError) -> Self {
        Error::from(value).into()
    }
}

impl From<TransactionError<Error>> for DbError {
    fn from(value: TransactionError<Error>) -> Self {
        match value {
            TransactionError::Abort(e) => Self::Ident(e),
            TransactionError::Storage(e) => Self::Sled(e),
        }
    }
}

pub fn into_cte<E: Into<Error>>(e: E) -> ConflictableTransactionError<Error> {
    ConflictableTransactionError::Abort(e.into())
}

#[derive(Debug, PartialEq, Error)]
pub enum KeystoreError {
    #[error("missing expected {0} entity")]
    MissingEntity(&'static str),

    #[error("sled: {0}")]
    Sled(#[from] sled::Error),

    #[error("failed to encode {0} entity: {1}")]
    EncodeEntity(&'static str, String),

    #[error("{0}")]
    Other(String),
}
